namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Finds and flags accessibility issues.
    /// </summary>
    public class FollowAccessibilityRules : BaseIntrospectionRule
    {
        // Lists of controls that can, or should, use an alternate for the AccessibleName.
        private static readonly string[] canUseAltControlForAccessibleName =
            new string[]
            {
                WellKnownTypeNames.CheckedListBox,
                WellKnownTypeNames.ComboBox,
                WellKnownTypeNames.DataGrid,
                WellKnownTypeNames.DateTimePicker,
                WellKnownTypeNames.ListBox,
                WellKnownTypeNames.ListView,
                WellKnownTypeNames.MaskedTextBox,
                WellKnownTypeNames.RichTextBox,
                WellKnownTypeNames.TextBox,
                WellKnownTypeNames.TreeView
            };

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="FollowAccessibilityRules"/> class.
        /// </summary>
        public FollowAccessibilityRules()
            : base(typeof(FollowAccessibilityRules).Name, "Jsl.FxCop.Rules", typeof(FollowAccessibilityRules).Assembly)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FollowAccessibilityRules"/> class.
        /// </summary>
        /// <param name="ruleType">Type of the rule.</param>
        /// <param name="resourceName">Name of the resource.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "How can I validate?")]
        public FollowAccessibilityRules(Type ruleType, string resourceName)
            : base(ruleType.Name, resourceName, ruleType.Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Ran after all analysis has been finished.
        /// </summary>
        public override void AfterAnalysis()
        {
            base.AfterAnalysis();
            ComponentsCache.Clear();
        }

        /// <summary>
        /// Checks the specified field.
        /// </summary>
        /// <param name="member">The member to check.</param>
        /// <returns>The problems found if any.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Justification = "Unnecessary.")]
        public override ProblemCollection Check(Member member)
        {
            member.DoIfUIField(this.ProcessField);

            return this.Problems;
        }
        #endregion Public Methods

        #region Protected Methods
        /// <summary>
        /// Determines whether the component can supply the AccessibleName to another component.
        /// </summary>
        /// <param name="component">The component.</param>
        /// <returns><c>true</c> if the component can supply the AccessibleName to another component.</returns>
        protected virtual bool CanSupplyAccessibilityName(ComponentInfo component)
        {
            if (component == null)
            {
                throw new ArgumentNullException("component");
            }

            return TypeNodeHelper.IsAssignableTo(component.Type, WellKnownTypeNames.Label);
        }

        /// <summary>
        /// Determines whether this control can use an alternate control for it's AccessibleName.
        /// </summary>
        /// <param name="controlType">Type of the control.</param>
        /// <returns><c>true</c> if this control can use an alternate control for it's AccessibleName.</returns>
        protected virtual bool CanUseAltControlForAccessibleName(TypeNode controlType)
        {
            // Look through the list of controls that can use a Label for the AccessibleName.
            foreach (var controlTypeName in FollowAccessibilityRules.canUseAltControlForAccessibleName)
            {
                if (TypeNodeHelper.IsAssignableTo(controlType, controlTypeName))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checks for preceding accessing name control.
        /// </summary>
        /// <param name="componentDictionary">The component dictionary.</param>
        /// <param name="field">The field.</param>
        /// <returns><c>true</c> if there was a valid preceding control.</returns>
        protected virtual bool CheckForPrecedingAccessingNameControl(ComponentDictionary componentDictionary, Field field)
        {
            if (componentDictionary == null)
            {
                throw new ArgumentNullException("componentDictionary");
            }

            if (field == null)
            {
                throw new ArgumentNullException("field");
            }

            // Get the control associated with the field, if any.
            ComponentInfo thisControl;
            if (!componentDictionary.TryGetValue(field, out thisControl))
            {
                return false;
            }

            // Find the best control to use for the AccessibleName by location and also by TabIndex.
            var bestLocationComponents = new List<ComponentInfo>();
            ComponentInfo bestTabIndexComponent = null;
            foreach (var siblingComponent in componentDictionary.EnumerateSiblings(thisControl))
            {
                // Only look at controls that can provide an AccessibilityName.
                if (this.CanSupplyAccessibilityName(siblingComponent))
                {
                    if (IsBetterTabIndex(thisControl, siblingComponent, bestTabIndexComponent))
                    {
                        bestTabIndexComponent = siblingComponent;
                    }

                    if (thisControl.IsNear(siblingComponent))
                    {
                        bestLocationComponents.Add(siblingComponent);
                    }
                }
            }

            // If we found a control that seems to be in the right location and tab order assume it is correct.
            if (bestLocationComponents.Contains(bestTabIndexComponent))
            {
                return true;
            }

            // If there was only one control found in the right location but it is different than the one
            // found by TabIndex then warn the user they probably need to swap.
            if ((bestLocationComponents.Count == 1) && (bestTabIndexComponent != null))
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            "FoundConflict",
                            bestLocationComponents[0].GetValue<string>("Name"),
                            bestTabIndexComponent.GetValue<string>("Name"),
                            field.Name.Name)));

                return false;
            }

            // If we found a control in the correct tab order by not location, log it.
            if (bestTabIndexComponent != null)
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            "FoundByTabIndex",
                            field.Name.Name,
                            bestTabIndexComponent.GetValue<string>("Name"))));

                return false;
            }

            // If there is only one control in the right position, suggest it to the user.
            if (bestLocationComponents.Count == 1)
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            "FoundOneByLocation",
                            field.Name.Name,
                            bestLocationComponents[0].GetValue<string>("Name"))));

                return false;
            }

            this.Problems.Add(new Problem(this.GetNamedResolution("NoAppropriateSibling", field.Name.Name)));
            return false;
        }

        /// <summary>
        /// Determines if the AccessibleName has been provided.
        /// </summary>
        /// <param name="componentDictionary">The component dictionary.</param>
        /// <param name="field">The field.</param>
        /// <returns><c>true</c> if [is accessible name provided] [the specified component dictionary]; otherwise, <c>false</c>.</returns>
        protected virtual bool IsAccessibleNameProvided(ComponentDictionary componentDictionary, Field field)
        {
            // If the AccessibleName property is set then it will be used so we don't have to check anything else.
            return !string.IsNullOrEmpty(componentDictionary.GetAccessibleName(field));
        }

        /// <summary>
        /// Determines if the field should be checked.
        /// </summary>
        /// <param name="componentDictionary">The component dictionary.</param>
        /// <param name="field">The field.</param>
        /// <returns><c>true</c> if the field should be checked.</returns>
        protected virtual bool ShouldCheckField(ComponentDictionary componentDictionary, Field field)
        {
            if (field == null)
            {
                throw new ArgumentNullException("field");
            }

            // Skip everything that isn't a control and isn't a TabStop.
            return TypeNodeHelper.IsAssignableTo(field.Type, WellKnownTypeNames.Control)
                && componentDictionary.IsTabStop(field);
        }
        #endregion Protected Methods

        #region Private Methods
        private static bool IsBetterTabIndex(
            ComponentInfo thisComponent,
            ComponentInfo siblingComponent,
            ComponentInfo bestTabIndexComponent)
        {
            // If the component we are looking at doesn't have a TabIndex then there can never be a "best" found.
            int thisTabIndex;
            if (!thisComponent.TryGetValue<int>("TabIndex", out thisTabIndex))
            {
                return false;
            }

            // If the sibling's TabIndex isn't less then the target's TabIndex then it isn't acceptable.
            int siblingTabIndex;
            if (!siblingComponent.TryGetValue<int>("TabIndex", out siblingTabIndex))
            {
                return false;
            }
            else if (siblingTabIndex >= thisTabIndex)
            {
                return false;
            }

            // If there isn't a "best" yet then this is it.
            if (bestTabIndexComponent == null)
            {
                return true;
            }

            // The sibling is better if it is closer to the target TabIndex.
            int bestTabIndex;
            if (bestTabIndexComponent.TryGetValue<int>("TabIndex", out bestTabIndex))
            {
                if (bestTabIndex < siblingTabIndex)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Processes the field.
        /// </summary>
        /// <param name="field">The field.</param>
        private void ProcessField(Field field)
        {
            var componentDictionary = ComponentsCache.GetComponentDictionary(field.DeclaringType);

            if (!this.ShouldCheckField(componentDictionary, field))
            {
                return;
            }

            if (this.IsAccessibleNameProvided(componentDictionary, field))
            {
                return;
            }

            // If this control should have a label in front of it, then check...
            if (this.CanUseAltControlForAccessibleName(field.Type))
            {
                this.CheckForPrecedingAccessingNameControl(componentDictionary, field);
            }
        }
        #endregion Private Methods
    }
}